package com.atguigu.tingshu.search.service.impl;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch._types.aggregations.LongTermsAggregate;
import co.elastic.clients.elasticsearch._types.aggregations.LongTermsBucket;
import co.elastic.clients.elasticsearch._types.aggregations.TopHitsAggregate;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.NestedQuery;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.CompletionSuggestOption;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.Suggester;
import co.elastic.clients.elasticsearch.core.search.Suggestion;
import co.elastic.clients.json.JsonData;
import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.album.client.AlbumInfoFeignClient;
import com.atguigu.tingshu.common.config.threadpool.ThreadPoolConfiguration;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.model.album.BaseCategory3;
import com.atguigu.tingshu.model.album.BaseCategoryView;
import com.atguigu.tingshu.model.search.AlbumInfoIndex;
import com.atguigu.tingshu.model.search.SuggestIndex;
import com.atguigu.tingshu.query.search.AlbumIndexQuery;
import com.atguigu.tingshu.search.factory.ScheduleTaskThreadFactory;
import com.atguigu.tingshu.search.runnable.RebuildDistriBloomFilterRunnable;
import com.atguigu.tingshu.search.service.SearchService;
import com.atguigu.tingshu.user.client.UserInfoFeignClient;
import com.atguigu.tingshu.vo.album.AlbumStatVo;
import com.atguigu.tingshu.vo.search.AlbumInfoIndexVo;
import com.atguigu.tingshu.vo.search.AlbumSearchResponseVo;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import jakarta.annotation.PostConstruct;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class SearchServiceImpl implements SearchService {

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    @Autowired
    private AlbumInfoFeignClient albumInfoFeignClient;

    @Autowired
    private UserInfoFeignClient userInfoFeignClient;

    @Autowired
    private ElasticsearchClient elasticsearchClient;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RBloomFilter distributedBloomFilter;

    /**
     * 在启动中，初始化布隆过滤器，将当前所有的专辑id都存入布隆过滤器当中
     *  @PostConstruct 的作用是在bean初始化之后执行，用于初始化布隆过滤器
     *  是bean的生命周期中的一部分
     */
    /*
    @PostConstruct
    public void initDistributedBloomFilter(){
        //1.获取专辑id集合
        List<Long> albumIdList = getAlbumIdList();
        if (!CollectionUtils.isEmpty(albumIdList)){
            for (Long id : albumIdList) {
                distributedBloomFilter.add(id);
            }
        }
        log.info("分布式布隆过滤器初始化完毕，内部id数量个数：" + distributedBloomFilter.count());
    }
    */


    /**
     * 延时任务
     * 重建布隆过滤器
     */
    @PostConstruct
    public void rebuildDistributedBloomFilter(){
        ScheduleTaskThreadFactory instance = ScheduleTaskThreadFactory.getInstance();
        long time = instance.diffTime(System.currentTimeMillis());
        instance.execute(new RebuildDistriBloomFilterRunnable(redissonClient,this,redisTemplate),time,TimeUnit.MILLISECONDS);
    }


    /**
     * 根据一级分类id获取首页展示的数据
     *
     * @param category1Id 一级分类id
     * @return 首页展示数据
     */
    @Override
    @SneakyThrows
    public List<Map<String, Object>> channel(Long category1Id) {
        /**
         * 具体思路：
         * 我们要获取当前一级id分类下的首页展示数据
         * 1.我们首页展示的数据为当前一级分类下的前7个三级分类的专辑  --> 存储在List<>当中
         * 2.然后我们还要展示每个三级分类id下的专辑的热度最高的前六名 --> 存储在Map<>当中
         * 3.最后包装好，返回一个List<Map<String,Object>>
         */

        //1.根据一级分类id，获取三级分类对象
        Result<List<BaseCategory3>> category3IdListResult = albumInfoFeignClient.getTop7Category3ByCategory1Id(category1Id);
        List<BaseCategory3> category3List = category3IdListResult.getData();
        Assert.notNull(category3List, "[SearchServiceImpl]-远程调用album微服务的[getTop7Category3ByCategory1Id]方法失败");

        //2.将当前的三级分类对象集合进行转换，获取一个c3Id的FieldValue集合 --> 未来es需要使用
        List<FieldValue> c3IdFieldValueList = category3List
                .stream()
                .map(c3 -> FieldValue.of(c3.getId()))
                .toList();

        //3.将List<BaseCategory3> 根据c3Id作为key，转换为一个map类型
        Map<Long, BaseCategory3> category3Map = category3List
                .stream()
                .collect(Collectors.toMap(BaseCategory3::getId, c3 -> c3));

        //4.构建es访问请求，并且设置我们查询的条件逻辑
        SearchRequest request = buildSearchRequest4Index(c3IdFieldValueList);

        //5.发送请求，获取es返回结果
        SearchResponse<AlbumInfoIndex> response = elasticsearchClient.search(request, AlbumInfoIndex.class);

        //6.根据返回结果，进行数据解析，封装我们需要的数据,并返回
        return parseSearchResponse2List(response, category3Map);
    }


    /**
     * 根据查询条件搜索专辑
     *
     * @param query 查询信息封装vo
     * @return 搜索结果
     */
    @Override
    @SneakyThrows
    public AlbumSearchResponseVo search(AlbumIndexQuery query) {
        //1.设置dsl语句，获取es请求对象
        SearchRequest request = buildSearchRequest4CategoryAlbumInfo(query);

        //2.根据请求对象，发送请求，获取请求结果
        SearchResponse<AlbumInfoIndex> response = elasticsearchClient.search(request, AlbumInfoIndex.class);

        //3.解析结果，封装为目标对象
        AlbumSearchResponseVo result = parseSearchResponse2AlbumSearchResponseVo(response);

        //4.对返回的结果进一步处理，为其中的分页数据进行赋值
        Integer pageSize = query.getPageSize();
        result.setPageNo(query.getPageNo());
        result.setPageSize(pageSize);
        Long totalSize = result.getTotal();
        Long totalPages = totalSize % pageSize == 0 ? totalSize / pageSize : totalSize / pageSize + 1;
        result.setTotalPages(totalPages);

        //5.返回数据
        return result;
    }


    /**
     * 搜索框搜索内容提示词回显
     *
     * @param keyword 搜索框输入内容
     * @return 提示词
     */
    @Override
    @SneakyThrows
    public Set<String> completeSuggest(String keyword) {

        //1.构建dsl请求
        SearchRequest request = buildSearchRequest4Suggest(keyword);

        //2.根据请求，获取返回结果
        SearchResponse<SuggestIndex> response = elasticsearchClient.search(request, SuggestIndex.class);

        //3.解析返回结果，封装为返回对象
        Set<String> result = parseSearchResponse2Set(response);

        //4.对返回的结果进行处理，若不满足10条结果，我们就自行填充，不论如何都填充到10条
        int size = result.size();
        if (size < 10) {
            SearchResponse<SuggestIndex> suppleResponse = elasticsearchClient.search(sb -> sb
                    .index("suggestinfo")
                    .query(qb -> qb
                            .match(mqb -> mqb
                                    .field("title")
                                    .query(keyword))), SuggestIndex.class);
            for (Hit<SuggestIndex> hit : suppleResponse.hits().hits()) {
                SuggestIndex source = hit.source();
                String title = source.getTitle();
                result.add(title);
                size++;
                if (size >= 10) break;
            }
        }

        //5.返回数据
        return result;
    }

    /**
     * 根据专辑id获取专辑详情信息
     * 2025.1.24 引入异步编排多线程 优化代码
     * 2025.2.6 引入redis缓存机制，优化代码
     * <p>
     * [问题1]：我们在使用分布式锁的时候，需要考虑到在回源查询过程中，如果业务执行异常，那么就会导致锁没有正常释放的情况
     * 在这种情况下就会导致锁没有释放，然后导致死锁问题
     * 【解决方案】：我们需要给分布式锁设置一个过期时间
     * 来实现客户端和服务端都对锁进行释放操作，即客户端手动delete锁，同时，服务端设置过期时间，使锁失效
     * <p>
     * [问题2]：我们如果项目运行过程中出了问题，或者说是因为网络波动的原因，导致抢到锁的线程(A线程)执行回源的过程时间非常的长，导致还没有回源做完
     * 锁就先到达过期时间，直接导致锁释放了，然后B线程争抢到锁就会进来，然后它正常执行回源操作
     * 此时A线程缓过劲来，继续往下执行释放分布式锁的操作，就会导致A线程删除了B线程的锁，这就是锁的误删问题
     * 【解决方案】：我们在加分布式锁的时候，即setnx的时候，给锁赋值一个独一无二的锁值，
     * 然后在线程准备释放锁的时候，要优先去判断一下锁的值是否是当前线程的锁值，如果是，才去删除锁，否则就不删除
     * <p>
     * [问题3]:我们在判断问题2当中时，使用的方法，即判断锁是否是当前线程的锁，如果是，释放锁，如果不是，不释放锁
     * 这种先判断再删除的操作，不具备原子性，依旧可能会出现问题
     * 【解决方案】：使用lua脚本来保证原子性
     *
     * @param albumId 专辑id
     * @return 详情信息
     */
    ThreadLocal<String> uniqueLockValueMap = new ThreadLocal<>();

    @Override
    @SneakyThrows
    public Map<String, Object> getAlbumInfoDetail(Long albumId) {

        //进来第一步，为了防止缓存穿透，即恶意用户使用各种形式尝试访问数据库中不存在的一个数据列
        //导致穿透缓存，所有的请求都打到数据库上，我们使用布隆过滤器来防止缓存穿透
        //即用户携带专辑id向我们后台发起请求，我们先去布隆过滤器中查找当前的专辑id是否存在
        //若存在，则允许访问，若不存在，那么就返回一个null值，防止请求打到数据库里
        boolean isAlbumIdLegal = distributedBloomFilter.contains(albumId);
        if (!isAlbumIdLegal){
            return null;
        }


        //定义redis中缓存的key
        String cacheKey = RedisConstant.CACHE_INFO_PREFIX + albumId;
        //定义redis中分布式锁的key
        String lockKey = RedisConstant.CACHE_LOCK_SUFFIX + albumId;

        Boolean lockFlag = false;

        String uniqueLockValue = null;

        //1.我们引入了redis缓存来优化查询效率，进来第一步我们先尝试去缓存中取值
        String cacheStrFromRedis = redisTemplate.opsForValue().get(cacheKey);

        //2.判断redis是否查询到值，如果查询到值那么就返回即可，若没有，那么就需要回源去数据库中取值
        if (!StringUtils.isEmpty(cacheStrFromRedis)) {
            return JSONObject.parseObject(cacheStrFromRedis);
        }

        /**
         * ps:此处引入一个逻辑，我们在其他没有第一次争抢到锁的线程，进行自旋尝试获取锁的过程中
         * 此处假设A线程第一次争抢到锁，BCDE其他线程进行自旋
         * 此时由于A线程进程卡住了，然后A的分布式锁由于过期时间到了，失效了，然后B线程争抢到了锁
         * 然后B线程就会往ThreadLocal当中set一个值，用的是是B线程的uniqueLockValue，即在B线程过程中生成的UUID值
         * 所以我们在每次准备去回源的过程中，都先需要去判断ThreadLocal中是否有值，如果有值那么就说明当前已经切换到其他线程了
         * 然后我们需要直接将lockFlag设置为true，这样子B线程才能打破锁，直接进行回源操作
         */
        String uniqueLockValueFromAnotherThread = uniqueLockValueMap.get();
        if (!StringUtils.isEmpty(uniqueLockValueFromAnotherThread)){
            log.info("当前线程{}争抢到了锁，现在由这个线程来进行执行回源操作", Thread.currentThread().getName());
            //如果代码步入到当前代码块，则说明当前是其他线程争抢到了锁
            //我们需要为当前线程开辟道路
            lockFlag = true;
            uniqueLockValue = uniqueLockValueFromAnotherThread;
        } else {
            //3.在回源去数据库取值的时候，我们要考虑一下多线程并发的情况，如果此时有高并发请求访问当前接口
            // 那么在并发量过高的时候，就会导致大量的请求去访问数据库，导致数据库压力过大，从而导致数据库崩溃 --> 即“缓存击穿”问题
            // 所以在进行回源查询的时候，我们在这里设置一个分布式锁，我们可以利用redis的setnx指令的特性来实现分布式锁
            uniqueLockValue = UUID.randomUUID().toString().replaceAll("-", "");
            lockFlag = redisTemplate.opsForValue().setIfAbsent(lockKey, uniqueLockValue, 10, TimeUnit.DAYS);
        }

        if (lockFlag) {
            //去数据库中查询数据
            Map<String, Object> albumInfoDetailFromDB = getAlbumInfoDetailFromDB(albumId);
            //将数据库中查询到的值存入redis缓存中
            redisTemplate.opsForValue().set(cacheKey, JSONObject.toJSONString(albumInfoDetailFromDB));
            //4. 在完成缓存操作后，我们需要释放分布式锁
            //释放分布式锁的时候我们要去判断当前锁是否是当前线程的锁，如果是，才去删除锁，否则就不删除
            //使用lua脚本保障原子性
            String luaScript4Deletelock = "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
            Long executeResult = redisTemplate.execute(new DefaultRedisScript<>(luaScript4Deletelock, Long.class), List.of(lockKey), uniqueLockValue);
            log.info("判断锁是否是当前的锁并删除，结果：{}", executeResult == 0 ? "非当前线程的锁，删除失败" : "删除成功");

            //5.由于我们使用了ThreadLocal，为了防止内存泄露的情况，在return之前，先调用ThreadLocal的remove方法，情况ThreadLocal中的数据
            uniqueLockValueMap.remove();

            return albumInfoDetailFromDB;
        }
        //4.如果在没有成功地竞争获取到锁的时候，我们就会走到else当中
        // 在此时我们要先尝试去redis缓存中取值，如果此时取到值了，那么就没有必要进行自旋
        // 如果缓存中还没有获取到值，那么我们就进行自旋，尝试去竞争这把锁
        String cacheStrFromRedisAgain = redisTemplate.opsForValue().get(cacheKey);
        if (!StringUtils.isEmpty(cacheStrFromRedisAgain)) {
            return JSONObject.parseObject(cacheStrFromRedisAgain);
        } else {
            //5.如果缓存中依旧没有值，我们就进行自旋争抢锁
            while (true) {
                //在自旋的过程中再尝试去缓存中取值，双重缓存验证机制
                if (!StringUtils.isEmpty(redisTemplate.opsForValue().get(cacheKey))) {
                    return JSONObject.parseObject(redisTemplate.opsForValue().get(cacheKey));
                }
                Boolean tryToGetLockAgain = redisTemplate.opsForValue().setIfAbsent(lockKey, uniqueLockValue, 10, TimeUnit.DAYS);
                if (tryToGetLockAgain) {
                    uniqueLockValueMap.set(uniqueLockValue);
                    break;
                }
            }
            return getAlbumInfoDetail(albumId);
        }
    }

    public List<Long> getAlbumIdList() {
        //获取所有的专辑ID集合
        Result<List<Long>> allAlbumIdListResult = albumInfoFeignClient.getAllAlbumIdList();
        List<Long> allAlbumIdListData = allAlbumIdListResult.getData();
        Assert.notNull(allAlbumIdListData,"[SearchServiceImpl]调用album微服务的[getAllAlbumIdList]方法出错");
        return allAlbumIdListData;
    }


    @NotNull
    private Map<String, Object> getAlbumInfoDetailFromDB(Long albumId) throws InterruptedException, ExecutionException {
        Map<String, Object> result = new HashMap<>();

        //1.需要返回值，因此我们需要使用supplyAsync
        CompletableFuture<Long> albumInfoFuture = CompletableFuture.supplyAsync(() -> {
            Result<AlbumInfo> albumInfoResult = albumInfoFeignClient.getAlbumInfo(albumId);
            AlbumInfo albumInfo = albumInfoResult.getData();
            Assert.notNull(albumInfo, "[SearchServiceImpl]-远程调用album微服务的[getAlbumInfo]方法失败");
            result.put("albumInfo", albumInfo);
            return albumInfo.getUserId();
        }, threadPoolExecutor);


        CompletableFuture<Void> albumStatFuture = CompletableFuture.runAsync(() -> {
            Result<AlbumStatVo> albumStatVoResult = albumInfoFeignClient.getAlbumStatVoByAlbumId(albumId);
            AlbumStatVo albumStatVo = albumStatVoResult.getData();
            Assert.notNull(albumStatVo, "[SearchServiceImpl]-远程调用album微服务的[getAlbumStatVoByAlbumId]方法失败");
            result.put("albumStatVo", albumStatVo);
        }, threadPoolExecutor);


        CompletableFuture<Void> categoryViewFuture = CompletableFuture.runAsync(() -> {
            Result<BaseCategoryView> categoryViewResult = albumInfoFeignClient.getCategoryView(albumId);
            BaseCategoryView categoryView = categoryViewResult.getData();
            Assert.notNull(categoryView, "[SearchServiceImpl]-远程调用album微服务的[getCategoryView]方法失败");
            result.put("baseCategoryView", categoryView);
        }, threadPoolExecutor);


        //接收albumInfoFuture的返回值，再根据userId去远程调用user微服务，获取用户信息
        CompletableFuture<Void> userInfoFuture = albumInfoFuture.thenAcceptAsync((userId) -> {
            Result<UserInfoVo> userInfoVoResult = userInfoFeignClient.getUserInfoVo(userId);
            UserInfoVo userInfoVo = userInfoVoResult.getData();
            Assert.notNull(userInfoVo, "[SearchServiceImpl]-远程调用user微服务的[getUserInfoVo]方法失败");
            result.put("announcer", userInfoVo);
        });

        CompletableFuture<Void> pause = CompletableFuture.allOf(albumInfoFuture, albumStatFuture, categoryViewFuture, userInfoFuture);
        //这里为什么选择使用pause.get()，而不使用pause.join()
        //join()和get()相比，get和join都会阻塞线程，都能满足我们当前的业务状况
        //但是get能够保留原始的异常信息并且抛出，但是join会将原始的异常信息包装成CompletionException，我们就丢失了原始异常信息
        //我们希望能够保留原始异常的输出信息，因此我们选择使用get()
//        pause.join();
        pause.get();
        return result;
    }

    /**
     * 根据请求，解析返回结果，封装为Set集合
     * set集合里面存title，即我们搜索栏回显的结果
     *
     * @param response es的返回结果
     * @return 封装对象
     */
    private Set<String> parseSearchResponse2Set(SearchResponse<SuggestIndex> response) {
        Map<String, List<Suggestion<SuggestIndex>>> suggestMap = response.suggest();
        List<Suggestion<SuggestIndex>> suggestionKeyword = suggestMap.get("suggestionKeyword");
        List<Suggestion<SuggestIndex>> suggestionKeywordSequence = suggestMap.get("suggestionKeywordSequence");
        List<Suggestion<SuggestIndex>> suggestionKeywordPinyin = suggestMap.get("suggestionKeywordPinyin");

        Set<String> result = new HashSet();

        //1.获取suggestionKeyword中的title
        for (Suggestion<SuggestIndex> item : suggestionKeyword) {
            List<CompletionSuggestOption<SuggestIndex>> options = item.completion().options();
            for (CompletionSuggestOption<SuggestIndex> option : options) {
                SuggestIndex source = option.source();
                String title = source.getTitle();
                result.add(title);
            }
        }
        //2.获取suggestionKeywordSequence中的title
        for (Suggestion<SuggestIndex> item : suggestionKeywordSequence) {
            List<CompletionSuggestOption<SuggestIndex>> options = item.completion().options();
            for (CompletionSuggestOption<SuggestIndex> option : options) {
                SuggestIndex source = option.source();
                String title = source.getTitle();
                result.add(title);
            }
        }
        //3.获取suggestionKeywordPinyin中的title
        for (Suggestion<SuggestIndex> item : suggestionKeywordPinyin) {
            List<CompletionSuggestOption<SuggestIndex>> options = item.completion().options();
            for (CompletionSuggestOption<SuggestIndex> option : options) {
                SuggestIndex source = option.source();
                String title = source.getTitle();
                result.add(title);
            }
        }
        //4.返回结果
        return result;
    }


    /**
     * 根据提示词，构建dsl请求
     *
     * @param keyword 查询内容
     * @return dsl请求
     */
    private SearchRequest buildSearchRequest4Suggest(String keyword) {
        Suggester.Builder suggesterBuilder = new Suggester.Builder();
        Suggester suggester = suggesterBuilder.suggesters("suggestionKeyword", fsb -> fsb.prefix(keyword).completion(csb -> csb.field("keyword").skipDuplicates(true)))
                .suggesters("suggestionKeywordSequence", fsb -> fsb.prefix(keyword).completion(csb -> csb.field("keywordSequence").skipDuplicates(true)))
                .suggesters("suggestionKeywordPinyin", fsb -> fsb.prefix(keyword).completion(csb -> csb.field("keywordPinyin").skipDuplicates(true))).build();

        SearchRequest.Builder searchRequestBuilder = new SearchRequest.Builder();
        SearchRequest request = searchRequestBuilder.suggest(suggester).build();
        System.out.println("提示词的dsl:" + request.toString());
        return request;
    }


    /**
     * 解析es的返回结果，封装为AlbumSearchResponseVo对象
     *
     * @param response es的返回结果
     * @return 封装为AlbumSearchResponseVo对象
     */
    private AlbumSearchResponseVo parseSearchResponse2AlbumSearchResponseVo(SearchResponse<AlbumInfoIndex> response) {
        //1.构建我们需要返回的对象
        AlbumSearchResponseVo result = new AlbumSearchResponseVo();

        //2.获取 total 总记录数
        long total = response.hits().total().value();

        //3.获取response的主体内容
        List<Hit<AlbumInfoIndex>> hits = response.hits().hits();
        //遍历集合，获取其中的每一个source，即AlbumInfoIndex，我们在其中将其转换为AlbumInfoIndexVo
        //并且添加到list中，用于后面给对象赋值
        List<AlbumInfoIndexVo> list = new ArrayList<>();

        for (Hit<AlbumInfoIndex> hit : hits) {
            AlbumInfoIndex albumInfoIndex = hit.source();
            AlbumInfoIndexVo vo = new AlbumInfoIndexVo();
            if (albumInfoIndex != null) {
                BeanUtils.copyProperties(albumInfoIndex, vo);
                //4.处理高亮处理的albumTitle结果，并且替换原来的albumTitle
                Map<String, List<String>> highlight = hit.highlight();
                List<String> albumTitle = highlight.get("albumTitle");
                if (!CollectionUtils.isEmpty(albumTitle)) {
                    String highLightedTitle = albumTitle.get(0);
                    vo.setAlbumTitle(highLightedTitle);
                }
                list.add(vo);
            }
        }

        //5.为返回结果赋值
        result.setTotal(total);
        result.setList(list);
        return result;
    }


    /**
     * 构建dsl请求
     * 请求用于返回某一特定三级分类下的专辑信息
     *
     * @param query 查询条件
     * @return 专辑信息
     */
    private SearchRequest buildSearchRequest4CategoryAlbumInfo(AlbumIndexQuery query) {
        SearchRequest.Builder searchRequestBuilder = new SearchRequest.Builder();
        BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder();

        //1.判断传来的查询条件中是否有关键字信息，若有，那么就构建dsl请求
        String keyword = query.getKeyword();
        if (!StringUtils.isEmpty(keyword)) {
            boolQueryBuilder.should(qb -> qb.match(mqb -> mqb.field("albumTitle").query(keyword)));
            boolQueryBuilder.should(qb -> qb.match(mqb -> mqb.field("albumIntro").query(keyword)));
            boolQueryBuilder.should(qb -> qb.match(mqb -> mqb.field("announcerName").query(keyword)));
        }

        //2.判断传来的查询条件中是否有分类信息，若有，就构建dsl请求
        Long category1Id = query.getCategory1Id();
        if (category1Id != null) {
            boolQueryBuilder.must(qb -> qb.term(tqb -> tqb.field("category1Id").value(category1Id)));
        }
        Long category2Id = query.getCategory2Id();
        if (category2Id != null) {
            boolQueryBuilder.must(qb -> qb.term(tqb -> tqb.field("category2Id").value(category2Id)));
        }
        Long category3Id = query.getCategory3Id();
        if (category3Id != null) {
            boolQueryBuilder.must(qb -> qb.term(tqb -> tqb.field("category3Id").value(category3Id)));
        }

        //3.判断查询条件中的标签集合，是否为空，若有，则构建dsl请求
        List<String> attributeList = query.getAttributeList();
        if (!CollectionUtils.isEmpty(attributeList)) {
            for (String attribute : attributeList) {
                String[] attr = attribute.split(":");
                String attributeId = attr[0];
                String valueId = attr[1];
                NestedQuery.Builder nestedQueryBuilder = new NestedQuery.Builder();
                nestedQueryBuilder.path("attributeValueIndexList")
                        .query(qb -> qb
                                .bool(bqb -> bqb
                                        .must(mqb -> mqb
                                                .term(tqb -> tqb
                                                        .field("attributeValueIndexList.attributeId")
                                                        .value(attributeId)))
                                        .must(mqb -> mqb
                                                .term(tqb -> tqb
                                                        .field("attributeValueIndexList.valueId")
                                                        .value(valueId)))));
                NestedQuery nestedQuery = nestedQueryBuilder.build();
                boolQueryBuilder.must(qb -> qb
                        .nested(nestedQuery));
            }


        }

        //4.获取分页使用的查询数据（必定存在该二者的值，因为这两个变量有默认值0和20）
        Integer pageNo = query.getPageNo();
        Integer pageSize = query.getPageSize();

        //5.先构建一个临时的SearchRequestBuilder，我们在其中设置索引库名以及目前收集到的查询条件,还有分页条件
        SearchRequest.Builder requestBuilder = searchRequestBuilder.index("albuminfo")
                .query(qb -> qb.bool(boolQueryBuilder.build()))
                .from((pageNo - 1) * pageSize)
                .size(pageSize);

        //6.我们接下来需要为请求builder设置排序条件以及高亮条件
        //综合排序[1:desc] 播放量[2:desc] 发布时间[3:desc]；asc:升序 desc:降序
        String order = query.getOrder();
        if (!StringUtils.isEmpty(order)) {
            String[] orderRule = order.split(":");
            //获取排序字段
            String orderField = orderRule[0];
            //获取排序类型
            SortOrder orderType = "desc".equals(orderRule[0]) ? SortOrder.Desc : SortOrder.Asc;

            switch (orderField) {
                case "1":
                    requestBuilder.sort(sob -> sob
                            .field(fsb -> fsb
                                    .field("hotScore")
                                    .order(orderType)));
                    break;
                case "2":
                    requestBuilder.sort(sob -> sob
                            .field(fsb -> fsb
                                    .field("playStatNum")
                                    .order(orderType)));
                    break;
                case "3":
                    requestBuilder.sort(sob -> sob
                            .field(fsb -> fsb
                                    .field("createTime")
                                    .order(orderType)));
            }
        } else {
            requestBuilder.sort(sob -> sob
                    .field(fsb -> fsb
                            .field("hotScore")
                            .order(SortOrder.Desc)));
        }

        //7.为请求构建高亮条件
        requestBuilder.highlight(hb -> hb
                .fields("albumTitle", hfb -> hfb
                        .preTags("<font style='color:red'>")
                        .postTags("</font>")));

        //8.返回最后的请求
        SearchRequest request = requestBuilder.build();
        System.out.println("最后构建的request请求为:" + request.toString());
        return request;
    }

    /**
     * 解析response，封装数据对象
     *
     * @param response     请求返回对象
     * @param category3Map 三级分类集合map
     * @return 封装数据，即首页需要展示的数据
     */
    private List<Map<String, Object>> parseSearchResponse2List(SearchResponse<AlbumInfoIndex> response, Map<Long, BaseCategory3> category3Map) {
        //0.定义要返回的对象
        List<Map<String, Object>> result = new ArrayList();

        //1.从response响应中获取我们想要的数据
        //1.1 从response中获取到category3IdAggs聚合结果集
        Aggregate category3IdAggs = response.aggregations().get("category3IdAggs");
        //2.从聚合结果集的类型转换为精确类型
        LongTermsAggregate lterms = category3IdAggs.lterms();
        //3.获取桶 bucket
        List<LongTermsBucket> bucketArray = lterms.buckets().array();
        //遍历桶中的内容，组合每个专辑对象，将信息放入Map<String, Object>当中，最后组成一个Map的List，并返回
        for (LongTermsBucket item : bucketArray) {
            //4.获取当前这个专辑的三级分类id
            long c3Id = item.key();

            Map<String, Object> map = new HashMap<>();
            //5.将三级分类对象存入map中
            map.put("baseCategory3", category3Map.get(c3Id));
            //6.获取子聚合对象
            Aggregate top6SortByHotScore = item.aggregations().get("top6SortByHotScore");
            //7.将子聚合对象类型转换为精确类型
            TopHitsAggregate topHitsAggregate = top6SortByHotScore.topHits();
            //8.获取到真正的数据集合,但是我们真正要的数据在source中
            List<Hit<JsonData>> hits = topHitsAggregate.hits().hits();

            //9.遍历数据集合，将数据封装到AlbumInfoIndex对象中，并且组装一个List来封装这些对象，并将List放入Map中
            List<AlbumInfoIndex> albumInfoIndices = new ArrayList<>();
            for (Hit<JsonData> hit : hits) {
                JsonData source = hit.source();
                //9.将JsonData类型的数据转换为AlbumInfoIndex类型
                AlbumInfoIndex albumInfoIndex = JSONObject.parseObject(source.toString(), AlbumInfoIndex.class);
                albumInfoIndices.add(albumInfoIndex);
            }
            map.put("list", albumInfoIndices);
            result.add(map);
        }
        /**
         * 我们的map要两个键值对
         * 一个是  "baseCategory3" --> 装三级分类对象
         * 一个l   "list",         --> 装专辑es库中的实体类对象集合，即List<AlbumInfoIndex>
         */

        return result;
    }

    /**
     * 构建 es 请求对象，并且设置dsl语句
     * 请求用于返回首页专辑信息
     * category3IdAggs  top6SortByHotScore
     *
     * @param c3IdFieldValueList c3Id的FieldValue类型的集合
     * @return es请求对象
     */
    private SearchRequest buildSearchRequest4Index(List<FieldValue> c3IdFieldValueList) {
        SearchRequest request = SearchRequest.of(sb -> sb.index("albuminfo")
                .query(qb -> qb
                        .terms(tqb -> tqb
                                .field("category3Id")
                                .terms(tqfb -> tqfb.value(c3IdFieldValueList))))
                .aggregations("category3IdAggs", ab -> ab
                        .terms(tab -> tab
                                .field("category3Id")
                                .size(c3IdFieldValueList.size()))
                        .aggregations("top6SortByHotScore", agb -> agb
                                .topHits(tab -> tab
                                        .size(6)
                                        .sort(sob -> sob
                                                .field(fsb -> fsb
                                                        .field("hotScore")
                                                        .order(SortOrder.Desc)))))
                )
        );

        return request;
    }
}
